package org.joge.core.draw.tilemap;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Properties;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;


import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * A layer of tiles on the map
 *
 * @author kevin
 * @author liamzebedee
 */
public class Layer
{

    /**
     * The code used to decode Base64 encoding
     */
    private static byte[] baseCodes = new byte[256];

    /**
     * Static initialiser for the codes created against Base64
     */
    static
    {
        for (int i = 0; i < 256; i++)
        {
            baseCodes[i] = -1;
        }
        for (int i = 'A'; i <= 'Z'; i++)
        {
            baseCodes[i] = (byte) (i - 'A');
        }
        for (int i = 'a'; i <= 'z'; i++)
        {
            baseCodes[i] = (byte) (26 + i - 'a');
        }
        for (int i = '0'; i <= '9'; i++)
        {
            baseCodes[i] = (byte) (52 + i - '0');
        }
        baseCodes['+'] = 62;
        baseCodes['/'] = 63;
    }
    /**
     * The map this layer belongs to
     */
    private final TiledMap map;
    /**
     * The index of this layer
     */
    public int index;
    /**
     * The name of this layer - read from the XML
     */
    public String name;
    /**
     * The tile data representing this data, index 0 = tileset, index 1 = tile
     * id
     */
    public int[][][] data;
    /**
     * The width of this layer
     */
    public int width;
    /**
     * The height of this layer
     */
    public int height;
    /**
     * The opacity of this layer (range 0 to 1)
     */
    public float opacity = 1;
    /**
     * The visibility of this layer
     */
    public boolean visible = true;
    /**
     * the properties of this layer
     */
    public Properties props;
    /**
     * The TiledMapPlus of this layer
     */
    private TiledMapPlus tmap;

    /**
     * Create a new layer based on the XML definition
     *
     * @param element The XML element describing the layer
     * @param map The map this layer is part of
     * @throws SlickException Indicates a failure to parse the XML layer
     */
    public Layer(TiledMap map, Element element)
    {
        this.map = map;
        if (map instanceof TiledMapPlus)
        {
            tmap = (TiledMapPlus) map;
        }
        name = element.getAttribute("name");
        width = Integer.parseInt(element.getAttribute("width"));
        height = Integer.parseInt(element.getAttribute("height"));
        data = new int[width][height][3];
        String opacityS = element.getAttribute("opacity");
        if (!opacityS.equals(""))
        {
            opacity = Float.parseFloat(opacityS);
        }
        if (element.getAttribute("visible").equals("0"))
        {
            visible = false;
        }

        // now read the layer properties
        Element propsElement = (Element) element.getElementsByTagName(
                "properties").item(0);
        if (propsElement != null)
        {
            NodeList properties = propsElement.getElementsByTagName("property");
            if (properties != null)
            {
                props = new Properties();
                for (int p = 0; p < properties.getLength(); p++)
                {
                    Element propElement = (Element) properties.item(p);

                    String name = propElement.getAttribute("name");
                    String value = propElement.getAttribute("value");
                    props.setProperty(name, value);
                }
            }
        }

        Element dataNode = (Element) element.getElementsByTagName("data").item(
                0);
        String encoding = dataNode.getAttribute("encoding");
        String compression = dataNode.getAttribute("compression");

        if (encoding.equals("base64") && compression.equals("gzip"))
        {
            try
            {
                Node cdata = dataNode.getFirstChild();
                char[] enc = cdata.getNodeValue().trim().toCharArray();
                byte[] dec = decodeBase64(enc);
                GZIPInputStream is = new GZIPInputStream(
                        new ByteArrayInputStream(dec));
                readData(is);
            } catch (IOException e)
            {
                System.out.println("Unable to decode base 64 block");
            }
        } else if (encoding.equals("base64") && compression.equals("zlib"))
        {
            Node cdata = dataNode.getFirstChild();
            char[] enc = cdata.getNodeValue().trim().toCharArray();
            byte[] dec = decodeBase64(enc);
            InflaterInputStream is = new InflaterInputStream(
                    new ByteArrayInputStream(dec));
            readData(is);
        } else
        {
            System.out.println("Unsupport tiled map type: " + encoding
                    + "," + compression + " (only gzip/zlib base64 supported)");
        }
    }

    /**
     * For reading decompressed, decoded Layer data into this layer
     *
     * @param is
     */
    protected void readData(InputStream is)
    {
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                int tileId = 0;
                try
                {
                    tileId |= is.read();
                    tileId |= is.read() << 8;
                    tileId |= is.read() << 16;
                    tileId |= is.read() << 24;
                } catch (Exception e)
                {
                    e.printStackTrace();
                }
                if (tileId == 0)
                {
                    data[x][y][0] = -1;
                    data[x][y][1] = 0;
                    data[x][y][2] = 0;
                } else
                {
                    int realTileId = tileId & 0x1FFFFFFF;
                    TileSet set = map.findTileSet(realTileId);
                    if (set != null)
                    {
                        data[x][y][0] = set.index;
                        data[x][y][1] = realTileId - set.firstGID;
                    }
                    data[x][y][2] = tileId;
                }
            }
        }
    }

    /**
     * Get the gloal ID of the tile at the specified location in this layer
     *
     * @param x The x coorindate of the tile
     * @param y The y coorindate of the tile
     * @return The global ID of the tile
     */
    public int getTileID(int x, int y)
    {
        return data[x][y][2];
    }

    /**
     * Set the global tile ID at a specified location
     *
     * @param x The x location to set
     * @param y The y location to set
     * @param tile The tile value to set
     */
    public void setTileID(int x, int y, int tile)
    {
        if (tile == 0)
        {
            data[x][y][0] = -1;
            data[x][y][1] = 0;
            data[x][y][2] = 0;
        } else
        {
            TileSet set = map.findTileSet(tile);

            data[x][y][0] = set.index; // tileSetIndex
            data[x][y][1] = tile - set.firstGID; // localID
            data[x][y][2] = tile; // globalID
        }
    }

    /**
     * Render a section of this layer
     *
     * @param x The x location to render at
     * @param y The y location to render at
     * @param sx The x tile location to start rendering
     * @param sy The y tile location to start rendering
     * @param width The number of tiles across to render
     * @param ty The line of tiles to render
     * @param lineByLine True if we should render line by line, i.e. giving us a
     * chance to render something else between lines
     * @param mapTileWidth the tile width specified in the map file
     * @param mapTileHeight the tile height specified in the map file
     */
    public void render(int x, int y, int sx, int sy, int width, int ty,
            boolean lineByLine, int mapTileWidth, int mapTileHeight)
    {
        for (int tileset = 0; tileset < map.getTileSetCount(); tileset++)
        {
            TileSet set = null;

            for (int tx = 0; tx < width; tx++)
            {
                if ((sx + tx < 0) || (sy + ty < 0))
                {
                    continue;
                }
                if ((sx + tx >= this.width) || (sy + ty >= this.height))
                {
                    continue;
                }

                if (data[sx + tx][sy + ty][0] == tileset)
                {
                    if (set == null)
                    {
                        set = map.getTileSet(tileset);
//                        set.tiles.startUse();
                    }

                    int sheetX = set.getTileX(data[sx + tx][sy + ty][1]);
                    int sheetY = set.getTileY(data[sx + tx][sy + ty][1]);
                    int tileOffsetY = set.tileHeight - mapTileHeight;

                    set.tiles.setAlpha(this.opacity); // Sets opacity/alpha value
                    set.tiles.drawSubImages(x + (tx * mapTileWidth), y
                            + (ty * mapTileHeight) - tileOffsetY, sheetX, sheetY);


                }
            }

            if (lineByLine)
            {
                if (set != null)
                {
                    set = null;
                }
                map.renderedLine(ty, ty + sy, index);
            }


        }
    }

    /**
     * Decode a Base64 string as encoded by TilED
     *
     * @param data The string of character to decode
     * @return The byte array represented by character encoding
     */
    private byte[] decodeBase64(char[] data)
    {
        int temp = data.length;
        for (int ix = 0; ix < data.length; ix++)
        {
            if ((data[ix] > 255) || baseCodes[data[ix]] < 0)
            {
                --temp;
            }
        }

        int len = (temp / 4) * 3;
        if ((temp % 4) == 3)
        {
            len += 2;
        }
        if ((temp % 4) == 2)
        {
            len += 1;
        }

        byte[] out = new byte[len];

        int shift = 0;
        int accum = 0;
        int index = 0;

        for (int ix = 0; ix < data.length; ix++)
        {
            int value = (data[ix] > 255) ? -1 : baseCodes[data[ix]];

            if (value >= 0)
            {
                accum <<= 6;
                shift += 6;
                accum |= value;
                if (shift >= 8)
                {
                    shift -= 8;
                    out[index++] = (byte) ((accum >> shift) & 0xff);
                }
            }
        }

        if (index != out.length)
        {
            throw new RuntimeException(
                    "Data length appears to be wrong (wrote " + index
                    + " should be " + out.length + ")");
        }

        return out;
    }

    /**
     * Gets all Tiles from this layer, formatted into Tile objects Can only be
     * used if the layer was loaded using TiledMapPlus
     *
     * @author liamzebedee
     * @throws SlickException
     */
    public ArrayList<Tile> getTiles()
    {
        if (tmap == null)
        {
            System.out.println("This method can only be used with Layers loaded using TiledMapPlus");
        }
        ArrayList<Tile> tiles = new ArrayList<Tile>();
        for (int x = 0; x < this.width; x++)
        {
            for (int y = 0; y < this.height; y++)
            {
                String tilesetName = tmap.tileSets.get(this.data[x][y][0]).name;
                Tile t = new Tile(x, y, this.name, y, tilesetName);
                tiles.add(t);
            }
        }
        return tiles;
    }

    /**
     * Get all tiles from this layer that are part of a tileset Can only be used
     * if the layer was loaded using TiledMapPlus
     *
     * @author liamzebedee
     * @param tilesetName The name of the tileset that the tiles are part of
     * @throws SlickException
     */
    public ArrayList<Tile> getTilesOfTileset(String tilesetName)
    {
        if (tmap == null)
        {
            System.out.println("This method can only be used with Layers loaded using TiledMapPlus");
        }
        ArrayList<Tile> tiles = new ArrayList<Tile>();
        int tilesetID = tmap.getTilesetID(tilesetName);
        for (int x = 0; x < tmap.getWidth(); x++)
        {
            for (int y = 0; y < tmap.getHeight(); y++)
            {
                if (this.data[x][y][0] == tilesetID)
                {
                    Tile t = new Tile(x, y, this.name, this.data[x][y][1],
                            tilesetName);
                    tiles.add(t);
                }
            }
        }
        return tiles;
    }

    /**
     * Removes a tile
     *
     * @author liamzebedee
     * @param x Tile X
     * @param y Tile Y
     */
    public void removeTile(int x, int y)
    {
        this.data[x][y][0] = -1;
    }

    /**
     * Sets a tile's tileSet Can only be used if the layer was loaded using
     * TiledMapPlus
     *
     * @author liamzebedee
     * @param x Tile X
     * @param y Tile Y
     * @param tileOffset The offset of the tile, within the tileSet to set this
     * tile to, ordered in rows
     * @param tilesetName The name of the tileset to set the tile to
     * @throws SlickException
     */
    public void setTile(int x, int y, int tileOffset, String tilesetName)
    {
        if (tmap == null)
        {
            System.out.println("This method can only be used with Layers loaded using TiledMapPlus");
        }
        int tilesetID = tmap.getTilesetID(tilesetName);
        TileSet tileset = tmap.getTileSet(tilesetID);
        this.data[x][y][0] = tileset.index; // tileSetIndex
        this.data[x][y][1] = tileOffset; // localID
        this.data[x][y][2] = tileset.firstGID + tileOffset; // globalID
    }

    /**
     * Returns true if this tile is part of that tileset Can only be used if the
     * layer was loaded using TiledMapPlus
     *
     * @author liamzebedee
     * @param x The x co-ordinate of the tile
     * @param y The y co-ordinate of the tile
     * @param tilesetName The name of the tileset, to check if the tile is part
     * of
     * @throws SlickException
     */
    public boolean isTileOfTileset(int x, int y, String tilesetName)
    {
        if (tmap == null)
        {
            System.out.println("This method can only be used with Layers loaded using TiledMapPlus");
        }
        int tilesetID = tmap.getTilesetID(tilesetName);
        if (this.data[x][y][0] == tilesetID)
        {
            return true;
        }
        return false;
    }
}